package org.codefilarete.stalactite.spring.repository.query.derivation;

import java.util.ArrayList;
import java.util.List;

import org.codefilarete.reflection.Accessor;
import org.codefilarete.reflection.AccessorChain;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.stalactite.query.model.ConditionalOperator;
import org.codefilarete.stalactite.query.model.operator.Between;
import org.codefilarete.stalactite.query.model.operator.Equals;
import org.codefilarete.stalactite.query.model.operator.Greater;
import org.codefilarete.stalactite.query.model.operator.In;
import org.codefilarete.stalactite.query.model.operator.InIgnoreCase;
import org.codefilarete.stalactite.query.model.operator.IsNull;
import org.codefilarete.stalactite.query.model.operator.Lesser;
import org.codefilarete.stalactite.query.model.operator.Like;
import org.codefilarete.tool.collection.Arrays;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.repository.query.parser.Part;
import org.springframework.data.repository.query.parser.Part.IgnoreCaseType;
import org.springframework.data.repository.query.parser.Part.Type;

/**
 * Base class to build a query from a method.
 * 
 * @param <T>
 * @author Guillaume Mary
 */
public abstract class AbstractDerivedQuery<T> {
	
	private final Class<T> entityType;
	
	public AbstractDerivedQuery(Class<T> entityType) {
		this.entityType = entityType;
	}
	
	public Criterion convertToCriterion(Part part) {
		return convertToCriterion(part.getType(), part.shouldIgnoreCase() != IgnoreCaseType.NEVER);
	}
	
	private Criterion convertToCriterion(Type type, boolean ignoreCase) {
		ConditionalOperator<?, ?> operator = null;
		switch (type) {
			case BETWEEN:
				operator = new Between<>();
				break;
			case IS_NOT_NULL:
				operator = new IsNull().not();
				break;
			case IS_NULL:
				operator = new IsNull();
				break;
			case BEFORE:
			case LESS_THAN:
				operator = new Lesser<>();
				break;
			case LESS_THAN_EQUAL:
				operator = new Lesser<>().equals();
				break;
			case AFTER:
			case GREATER_THAN:
				operator = new Greater<>();
				break;
			case GREATER_THAN_EQUAL:
				operator = new Greater<>().equals();
				break;
			case NOT_LIKE:
				operator = new Like<>(false, false).not();
				if (ignoreCase) {
					operator = ((Like) operator).ignoringCase();
				}
				break;
			case LIKE:
				operator = new Like<>(false, false);
				if (ignoreCase) {
					operator = ((Like) operator).ignoringCase();
				}
				break;
			case STARTING_WITH:
				operator = Like.startsWith();
				break;
			case ENDING_WITH:
				operator = Like.endsWith();
				break;
			case IS_NOT_EMPTY:
				break;
			case IS_EMPTY:
				break;
			case NOT_CONTAINING:
				operator = Like.contains().not();
				break;
			case CONTAINING:
				operator = Like.contains();
				break;
			case NOT_IN:
				operator = new In<>().not();
				if (ignoreCase) {
					operator = ((In) operator).ignoringCase();
				}
				break;
			case IN:
				operator = new In<>();
				if (ignoreCase) {
					operator = ((In) operator).ignoringCase();
				}
				break;
			case TRUE:
				operator = new Equals<>(true);
				break;
			case FALSE:
				operator = new Equals<>(false);
				break;
			case NEGATING_SIMPLE_PROPERTY:
				operator = new Equals<>().not();
				if (ignoreCase) {
					operator = ((Equals) operator).ignoringCase();
				}
				break;
			case SIMPLE_PROPERTY:
				operator = new Equals<>();
				if (ignoreCase) {
					operator = ((Equals) operator).ignoringCase();
				}
				break;
			// Hereafter operators are not supported for different reasons :
			// - JPA doesn't either,
			// - too database-specific,
			// - don't know on which operator to bind them (no javadoc)
			case NEAR:
			case WITHIN:
			case REGEX:
			case EXISTS:
		}
		if (operator == null) {
			throw new UnsupportedOperationException("Unsupported operator type: " + type);
		} else {
			// Adapting result depending on kind of operator was instantiated
			if (operator instanceof Between) {
				return new BetweenCriterion((Between<?>) operator);
			} else if (operator instanceof In || operator instanceof InIgnoreCase) {
				return new InCriterion(operator);
			} else if (operator instanceof IsNull) {
				return new IsNullCriterion((IsNull) operator);
			} else if (type == Type.TRUE || type == Type.FALSE) {
				return new BooleanCriterion(operator);
			} else {
				return new Criterion(operator);
			}
		}
	}
	
	protected <O> AccessorChain<T, O> convertToAccessorChain(Order property) {
		return this.convertToAccessorChain(PropertyPath.from(property.getProperty(), entityType));
	}
	
	protected <O> AccessorChain<T, O> convertToAccessorChain(PropertyPath property) {
		List<Accessor<?, ?>> accessorChain = new ArrayList<>();
		property.forEach(path ->
				accessorChain.add(Accessors.accessor(path.getOwningType().getType(), path.getSegment())));
		return new AccessorChain<>(accessorChain);
	}
	
	/**
	 * Combination of an operator and its expected argument count.
	 * Helps to consume runtime arguments, see {@link #consume(Object[])}
	 *
	 * @author Guillaume Mary
	 */
	public static class Criterion {
		
		protected final ConditionalOperator<Object, Object> condition;
		protected final int argumentCount;
		
		private Criterion(ConditionalOperator<?, ?> condition) {
			this(condition, 1);
		}
		
		public Criterion(ConditionalOperator<?, ?> condition, int argumentCount) {
			this.condition = (ConditionalOperator<Object, Object>) condition;
			this.argumentCount = argumentCount;
		}
		
		public void setValue(Object[] arguments, int argumentIndex) {
			this.condition.setValue(convert(arguments, argumentIndex));
		}
		
		protected Object convert(Object[] arguments, int argumentIndex) {
			return arguments[argumentIndex];
		}
	}
	
	private static class BetweenCriterion extends Criterion {
		
		public BetweenCriterion(Between<?> operator) {
			super(operator, 2);
		}

		@Override
		public Object convert(Object[] arguments, int argumentIndex) {
			return new Between.Interval<>(arguments[argumentIndex], arguments[argumentIndex + 1]);
		}
	}

	private static class IsNullCriterion extends Criterion {
		
		public IsNullCriterion(IsNull operator) {
			super(operator, 0);
		}

		@Override
		public void setValue(Object[] arguments, int argumentIndex) {
			// IsNull doesn't set any value (at least it does nothing)
			// moreover default behavior will throw exception due to argument position computation:
			// findByNameIsNull() has no arg => arguments is empty making arguments[0] throws an IndexOutOfBoundsException 
		}
	}

	private static class InCriterion extends Criterion {
		public InCriterion(ConditionalOperator<?, ?> operator) {
			super(operator, 2);
		}

		@Override
		public Object convert(Object[] arguments, int argumentIndex) {
			Object argument = arguments[argumentIndex];
			if (argument instanceof Object[]) {
				// converting varargs to an Iterable because it's what's expected by "In" class as value
				return Arrays.asList((Object[]) argument);
			} else {
				return argument;
			}
		}
	}

	private static class BooleanCriterion extends Criterion {
		public BooleanCriterion(ConditionalOperator<?, ?> operator) {
			super(operator, 0);
		}

		@Override
		public void setValue(Object[] arguments, int argumentIndex) {
			// TRUE and FALSE don't set any value
			// moreover default behavior will throw exception due to argument position computation:
			// findByNameIsNull() has no arg => arguments is empty making arguments[0] throws an IndexOutOfBoundsException 
		}
	}
}
